/* Copyright (c) 2006, 2009, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
 
package com.cburch.logisim.instance;

import java.awt.Graphics;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.comp.ComponentEvent;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.comp.ComponentListener;
import com.cburch.logisim.comp.ComponentUserEvent;
import com.cburch.logisim.comp.EndData;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeEvent;
import com.cburch.logisim.data.AttributeListener;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.tools.TextEditable;
import com.cburch.logisim.tools.ToolTipMaker;
import com.cburch.logisim.collections.EventSourceWeakSupport;
import com.cburch.logisim.collections.UnmodifiableList;

public class InstanceComponent implements Component, AttributeListener, ToolTipMaker {
	// TODO: Incorporate Generics
	
    private EventSourceWeakSupport listeners;
    private InstanceFactory factory;
    private Instance instance;
    private Location loc;
    private Bounds bounds;
    private List portList;
    private EndData[] endArray;
    private List endList;
    private boolean hasToolTips;
    private HashSet widthAttrs;
    private AttributeSet attrs;
    private boolean attrListenRequested;
    private InstanceTextField textField;
    
    InstanceComponent(InstanceFactory factory, Location loc,
            AttributeSet attrs) {
        this.listeners = null;
        this.factory = factory;
        this.instance = new Instance(this);
        this.loc = loc;
        this.bounds = factory.getOffsetBounds(attrs).translate(loc.getX(), loc.getY());
        this.portList = factory.getPorts();
        this.endArray = null;
        this.hasToolTips = false;
        this.attrs = attrs;
        this.attrListenRequested = false;
        this.textField = null;

        computeEnds();
    }
    
    private void computeEnds() {
        List ports = portList;
        EndData[] esOld = endArray;
        int esOldLength = esOld == null ? 0 : esOld.length;
        EndData[] es = esOld;
        if(es == null || es.length != ports.size()) {
            es = new EndData[ports.size()];
            if(esOldLength > 0) {
                int toCopy = Math.min(esOldLength, es.length);
                System.arraycopy(esOld, 0, es, 0, toCopy);
            }
        }
        HashSet wattrs = null;
        boolean toolTipFound = false;
        ArrayList endsChangedOld = null;
        ArrayList endsChangedNew = null;
        Iterator pit = ports.iterator();
        for(int i = 0; pit.hasNext() || i < esOldLength; i++) {
            Port p = pit.hasNext() ? (Port) pit.next() : null;
            EndData oldEnd = i < esOldLength ? esOld[i] : null;
            EndData newEnd = p == null ? null : p.toEnd(loc, attrs);
            if(oldEnd == null || !oldEnd.equals(newEnd)) {
                if(newEnd != null) es[i] = newEnd;
                if(endsChangedOld == null) {
                    endsChangedOld = new ArrayList();
                    endsChangedNew = new ArrayList();
                }
                endsChangedOld.add(oldEnd);
                endsChangedNew.add(newEnd);
            }
            
            if(p != null) {
                Object attr = p.getWidthAttribute();
                if(attr != null) {
                    if(wattrs == null) wattrs = new HashSet();
                    wattrs.add(attr);
                }
                
                if(p.getToolTip() != null) toolTipFound = true;
            }
        }
        if(!attrListenRequested) {
            HashSet oldWattrs = widthAttrs;
            if(wattrs == null && oldWattrs != null) {
                getAttributeSet().removeAttributeListener(this);
            } else if(wattrs != null && oldWattrs == null) {
                getAttributeSet().addAttributeListener(this);
            }
        }
        if(es != esOld) {
            endArray = es;
            endList = new UnmodifiableList(es);
        }
        widthAttrs = wattrs;
        hasToolTips = toolTipFound;
        if(endsChangedOld != null) {
            fireEndsChanged(endsChangedOld, endsChangedNew);
        }
    }
    
    //
    // listening methods
    //
    public void addComponentListener(ComponentListener l) {
        if(listeners == null) listeners = new EventSourceWeakSupport();
        listeners.add(l);
    }

    public void removeComponentListener(ComponentListener l) {
        if(listeners != null) {
            listeners.remove(l);
            if(listeners.size() == 0) listeners = null;
        }
    }
    
    private void fireEndsChanged(ArrayList oldEnds, ArrayList newEnds) {
        EventSourceWeakSupport ls = listeners;
        if(ls != null) {
            ComponentEvent e = null;
            for(Iterator it = ls.iterator(); it.hasNext(); ) {
                ComponentListener l = (ComponentListener) it.next();
                if(e == null) e = new ComponentEvent(this, oldEnds, newEnds);
                l.endChanged(e);
            }
        }
    }
    
    void fireInvalidated() {
        EventSourceWeakSupport ls = listeners;
        if(ls != null) {
            ComponentEvent e = null;
            for(Iterator it = ls.iterator(); it.hasNext(); ) {
                ComponentListener l = (ComponentListener) it.next();
                if(e == null) e = new ComponentEvent(this);
                l.componentInvalidated(e);
            }
        }
    }

    //
    // basic information methods
    //
    public ComponentFactory getFactory() {
        return factory;
    }

    public AttributeSet getAttributeSet() {
        return attrs;
    }
    
    public Object getAttributeValue(Attribute attr)
    {
    	return this.getAttributeSet().getValue(attr);
    }

    public void setAttributeValue(Attribute attr, Object value)
    {
        this.getAttributeSet().setValue(attr, value);
    }
    
    public Object getFeature(Object key) {
        Object ret = factory.getInstanceFeature(instance, key);
        if(ret != null) {
            return ret;
        } else if(key == ToolTipMaker.class) {
            if(hasToolTips) return this;
        } else if(key == TextEditable.class) {
            InstanceTextField field = textField;
            if(field != null) return field;
        }
        return null;
    }

    //
    // location/extent methods
    //
    public Location getLocation() {
        return loc;
    }
    
    public Bounds getBounds() {
        return bounds;
    }

    public Bounds getBounds(Graphics g) {
        Bounds ret = bounds;
        InstanceTextField field = textField;
        if(field != null) ret = ret.add(field.getBounds(g));
        return ret;
    }

    public boolean contains(Location pt) {
        Bounds bds = getBounds();
        if(bds == null) return false;
        return bds.contains(pt, 1);
    }
    
    public boolean contains(Location pt, Graphics g) {
        InstanceTextField field = textField;
        if(field != null && field.getBounds(g).contains(pt)) return true;
        else return contains(pt);
    }

    //
    // propagation methods
    //
    public List getEnds() {
        return endList;
    }

    public EndData getEnd(int index) {
        return endArray[index];
    }

    public boolean endsAt(Location pt) {
        EndData[] ends = endArray;
        for(int i = 0; i < ends.length; i++) {
            if(ends[i].getLocation().equals(pt)) return true;
        }
        return false;
    }

    public void propagate(CircuitState state) {
        factory.propagate(new InstanceStateImpl(state, this));
    }
    
    public void destroyComponent(Component comp)
    {
    	factory.destroyComponent(comp);
    }

    //
    // drawing methods
    //
    public void draw(ComponentDrawContext context) {
        InstancePainter painter = context.getInstancePainter();
        painter.setInstance(this);
        factory.paintInstance(painter);
    }

    public void expose(ComponentDrawContext context) {
        Bounds b = bounds;
        context.getDestination().repaint(b.getX(), b.getY(), b.getWidth(), b.getHeight());
    }
    
    public String getToolTip(ComponentUserEvent e) {
        int x = e.getX();
        int y = e.getY();
        EndData[] es = endArray;
        for(int i = 0; i < es.length; i++) {
            if(es[i].getLocation().manhattanDistanceTo(x, y) < 10) {
                Port p = (Port) portList.get(i);
                return p.getToolTip();
            }
        }
        return null;
    }

    //
    // AttributeListener methods
    //
    public void attributeListChanged(AttributeEvent e) { }

    public void attributeValueChanged(AttributeEvent e) {
        Attribute attr = e.getAttribute();
        if(widthAttrs != null && widthAttrs.contains(attr)) computeEnds();
        if(attrListenRequested) {
            factory.instanceAttributeChanged(instance, e.getAttribute());
        }
    }
    
    //
    // methods for InstancePainter
    //
    void drawLabel(ComponentDrawContext context) {
        InstanceTextField field = textField;
        if(field != null) field.draw(this, context);
    }

    //
    // methods for Instance
    //
    public Instance getInstance() {
        return instance;
    }
    
    List getPorts() {
        return portList;
    }
    
    void setPorts(Port[] ports) {
        Port[] portsCopy = (Port[]) ports.clone();
        portList = new UnmodifiableList(portsCopy);
        computeEnds();
    }
    
    void recomputeBounds() {
        Location p = loc;
        bounds = factory.getOffsetBounds(attrs).translate(p.getX(), p.getY());
    }
    
    void addAttributeListener(Instance instance) {
        if(!attrListenRequested) {
            attrListenRequested = true;
            if(widthAttrs == null) getAttributeSet().addAttributeListener(this);
        }
    }
    
    void setTextField(Attribute labelAttr, Attribute fontAttr,
            int x, int y, int halign, int valign) {
        InstanceTextField field = textField;
        if(field == null) {
            field = new InstanceTextField(this);
            field.update(labelAttr, fontAttr, x, y, halign, valign);
            textField = field;
        } else {
            field.update(labelAttr, fontAttr, x, y, halign, valign);
        }
    }

}
